/*
 * Main.java
 *
 * Created on May 28, 2008, 1:15 PM
 *
 * by Jakub Wartak 2008-2009, Licesed under GPLv2 terms
 *
 * 0.0.2 - PUBLIC
 *      conf.oracleHome is now taken from $ORACLE_HOME,
 *      ConnectThread with FcfThread interval is now conf.delay, not 1 second,
 *      listenerPort parameter is not hardcored (-P)
 *
 * 0.0.3 - NOT RELEASED/PRIVATE
 *      new option: "-e" => specifying PL/SQL procedure execution
 *
 * 0.0.4 - INTERNAL USE MAINLY
 *      added monitoring for commits & rollbacks
 *      added warning message for too short monitoring interval
 *      new option: "-n" => queries/DML/PLSQL are by default run three times to
 *          get accurate/average timings
 *      new option: "-f" => writing to stderr when file trigger appears (mainly for
 *          monitoring correlations between "artifical" actions and latency times in
 *          gnuplot)
 *
 * 0.0.5/06.06.2010
 *      bug fixes
 *
 */

package orajdbcstat;

import java.sql.*;
import java.io.*;
import java.text.SimpleDateFormat;
import java.util.*;
import gnu.getopt.Getopt;
import oracle.jdbc.driver.*;
import oracle.jdbc.pool.*;
import oracle.ons.Subscriber;


public class Main {
    String VERSION = "0.0.5";
    OracleDataSource ods = null;
    
    Database DB = null;
    ArrayList<Thread> Threads = new ArrayList<Thread>();
    Configuration conf = new Configuration();
    
    private void Setup(Connection db) {
        
    }
    
    private void Sleep(Integer secs) {
        try {
            Thread.sleep(secs * 1000);
        } catch (InterruptedException e) {
            
        }
    }
    
    private String Long2TimedString(Long arg0) {
        
        /* error condition from thread */
        if(arg0 < 0) {
            arg0 = -arg0;
            return new String("E!" + String.format("%05d", arg0));
        }
        
        return arg0.toString();
    }
    
    private String StringRACid(Integer oldRac, Integer newRac) {
        if(newRac == oldRac) {
            /* currently no database switch happened */
            if(newRac == -1) {
                return String.format("%4c", 'X');
            } else {
                return String.format("%4d", newRac);
            }
        } else {
            /* we've just failovered */
            String from = oldRac.toString();
            String to = newRac.toString();
            if(oldRac == -1) {
                from = "X";
            }
            
            if(newRac == -1) {
                to = "X";
            }
            return new String(from + "->" + to);
        }
    }
    
    public void Usage() {
        System.out.println("orajdbcstat version " + VERSION + " by Jakub Wartak, 2008-2010");
        System.out.println();
        System.out.println("usage: orajdbcstat.sh -d <DB_TNS> [-i <TNS instance #1> [-i <TNS instance #2>] ... ]");
        System.out.println("\t[-l <login>] [-p <pass>] [-s <delay>] [-S <delay>] [-o <ONS_configuration>]  ");
        System.out.println("\t[-m] [-e <PLSQL_procedure> [-M <num>] [-L <num>]] [-n <no_of_iterations>] ");
        System.out.println("\t[-v <level>] [-f <file>] [-h]");
        
        System.out.println();
        System.out.println("\t-d Oracle database TNS descriptor (required)");
        System.out.println("\t-i Oracle database specific TNS instance descrptor (RAC)");
        System.out.println("\t-P Listener port for TCP connect probe");
        System.out.println("\t-l Login used to perform queries");
        System.out.println("\t-p Password to access account");
        System.out.println("\t-s Delay between reports (seconds)");
        System.out.println("\t-S Number of seconds for internal Oracle JDBC timeouts (default: 20)");
        System.out.println("\t-n No. of iterations to perform for calc. avg query/DML/PLSQL timing (default: 1)");
        System.out.println("\t-o Oracle Notifications Services (ONS) hosts specification (RAC)");
        System.out.println("\t-v Verbose output");
        System.out.println("\t-e PL/SQL procedure name in <login> schema (by default no procedure is used)");
        System.out.println("\t-M Minimal random argument passed to PL/SQL procedure (default: 0)");
        System.out.println("\t-L Largest random argument passed to PL/SQL procedure (default: 1000)");
        System.out.println("\t-m Machine like output (typically for gnuplot)");
        System.out.println("\t-f File trigger to output on stderr with timestamp (default: disabled)");
        System.out.println("\t-h Usage information");
        System.out.println();
        System.exit(-1);
    }
    
    public Main(String args[]) {
        boolean run = true;
        boolean first = true;
        boolean machine = false;
        int ONSCount = 0;
        int header_count = 0;
        Integer machineTime = 0;
        ConnectThread DBConnectThread = null;
        FcfThread DBFCFThread = null;
        Thread thread_DB = null;
        Thread thread_FCF = null;
        
        Getopt g = new Getopt("orajdbcstats", args, "s:S:P:d:t:l:p:v:i:o:hme:M:L:n:f:");
        int ch;
        
        while ((ch = g.getopt()) != -1) {
            switch (ch) {
                case 's':
                    conf.delay = new Integer(g.getOptarg().toString());
                    conf.maxDelay = 3*conf.delay;
                    break;
                case 'S':
                    conf.networkDelay = new Integer(g.getOptarg().toString());
                    break;
                case 'P':
                    conf.listenerPort = new Integer(g.getOptarg().toString());
                    break;
                case 'l':
                    conf.login = g.getOptarg().toString();
                    break;
                case 'p':
                    conf.pass = g.getOptarg().toString();
                    break;
                case 'v':
                    conf.verbose = new Integer(g.getOptarg().toString());
                    break;
                case 'd':
                    DB = new Database(g.getOptarg().toString());
                    break;
                case 'i':
                    if(DB == null) {
                        System.err.println("orajdbcstat: you must give main database TNS descriptor first!");
                        System.exit(1);
                    }
                    Instance i = new Instance(g.getOptarg().toString());
                    DB.instances.add(i);
                    break;
                case 'o':
                    if(ONSCount == 0) {
                        conf.ONSConfiguration = new String("nodes=" + g.getOptarg().toString());
                    } else {
                        conf.ONSConfiguration.concat(g.getOptarg().toString());
                    }
                    ONSCount++;
                    break;
                case 'h':
                    Usage();
                    break;
                case 'm':
                    machine = true;
                    break;
                case 'e':
                    conf.plsqlProc = g.getOptarg().toString();
                    break;
                case 'M':
                    conf.minRand = new Integer(g.getOptarg().toString());
                    break;
                case 'L':
                    conf.maxRand = new Integer(g.getOptarg().toString());
                    break;
                case 'n':
                    conf.iterations = new Integer(g.getOptarg().toString());
                    break;
                case 'f':
                    conf.fileTrigger = g.getOptarg().toString();
                    break;
                default:
                    Usage();
                    break;
            }
        }
        
        try {
            conf.oracleHome = System.getenv("ORACLE_HOME");
            if(conf.oracleHome.equals("")) {
                throw new Exception("ORACLE_HOME empty!");
            }
        } catch(Exception e) {
            System.err.println("orajdbcstat: no ORACLE_HOME env set!");
            System.exit(1);
        }
        
        /* FIXME: description + parametrization */
        try {
            System.setProperty("oracle.ons.oraclehome", conf.oracleHome);
            //System.setProperty("oracle.net.ns.SQLnetDef.TCP_CONNTIMEOUT_STR", conf.networkDelay.toString());
            System.setProperty("oracle.net.READ_TIMEOUT", conf.networkDelay.toString());
        } catch (Exception e) {
            System.err.println("orajdbcstat: failed initialization of oracle JDBC properties: " + e.toString());
            System.exit(1);
        }
        
        if(DB == null) {
            System.err.println("orajdbcstat: no database TNS descriptor given!");
            Usage();
            System.exit(1);
        }
        
        if(conf.ONSConfiguration != null) {
            conf.ONSConfiguration.concat(";");
        }
        
        try {
            /* For manual direct connections, without pool */
            DriverManager.registerDriver( new oracle.jdbc.driver.OracleDriver() );
            
        } catch (SQLException e) {
            System.err.println(e.getMessage());
            System.exit(100);
        }
        
        /* ConnectThread with FCFThread for global TNS database descriptor */
        try {
            DBConnectThread = new ConnectThread((Instance) DB, conf);
            DBFCFThread = new FcfThread((Instance) DB, conf);
            thread_DB = new Thread(DBConnectThread);
            thread_FCF = new Thread(DBFCFThread);
            thread_DB.start();
            thread_FCF.start();
            Threads.add(thread_DB);
            Threads.add(thread_FCF);
        } catch(Exception e ) {
            System.err.println("orajdbcstat: unable to create main TNS java threads: " + e.getMessage());
            System.exit(1);
        }
        
        /* For each instance we spawn ConnectThread and FCF Thread */
        for (Instance i : DB.instances) {
            i.cthr = new ConnectThread(i, conf);
            i.fcfthr = new FcfThread(i, conf);
            
            Thread connThreadHndl = new Thread(i.cthr);
            Thread fcfThreadHndl = new Thread(i.fcfthr);
            connThreadHndl.start();
            fcfThreadHndl.start();
            Threads.add(connThreadHndl);
            Threads.add(fcfThreadHndl);
        }
        
        /* Start ONS thread */
        ONSThread ONS = new ONSThread(conf);
        Thread subscriberThread = new Thread(ONS);
        subscriberThread.start();

        try {
            Thread.sleep(conf.delay*1000);
        } catch(Exception e) {
            System.err.print(e.toString());
            System.exit(5);
        }
        
        while (run) {
            String time;
            if(machine == false && (header_count == 0 || (header_count > (21 / (DB.instances.size() + 1))))) {
                System.out.println("         -------- --- conntime [ms] --  -- FCF [ms] ---   - trx -");
                System.out.println("         tns      rac# tcpping    jdbc  rac#  txt plsql   cs   rs");
                
                header_count = 1;
            }
            header_count++;
            
            if(machine == true) {
                time = String.format("%8s", machineTime++);
            } else {
                SimpleDateFormat sdf = new SimpleDateFormat("HH:mm:ss");
                time = sdf.format(Calendar.getInstance().getTime());
            }
            
            
            /* Main DB handle, don't display if this is dry run */
            if(first == false) {
                String StatementTimes = null;
                String racId = null;
                String s_txt, s_exePLSQL;
                Long txt  = DBFCFThread.getTransactionTime(); /* TransaXtion Time */
                Long exePLSQL = DBFCFThread.getExecTime(); /* PLSQL exec time */
                Long cs = DBFCFThread.getCommits();
                Long rs = DBFCFThread.getRollbacks();
                
                Integer newRac = DBFCFThread.getStatementInstance();
                Integer oldRac = DBFCFThread.getOldStatementInstance();
                
                /* handle errors , passed by minus numbers, Long2TimedString()
                 * actually parses it for us
                 */
                if(txt < 0)  {
                    s_txt = Long2TimedString(txt);
                } else {
                    s_txt = String.format("%4d", txt);
                }
                
                if(exePLSQL < 0) {
                    s_exePLSQL = Long2TimedString(exePLSQL);
                } else {
                    s_exePLSQL = String.format("%5d", exePLSQL);
                }

                /* we display only the error in case we've got SQLexception */
                if(txt < 0 || exePLSQL < 0) {
                    StatementTimes = String.format("%21s", s_txt);
                } else {
                    StatementTimes = String.format("%s %s  %4d %4d", s_txt, s_exePLSQL, cs, rs);
                }
                
                racId = StringRACid(oldRac, newRac);
                
                System.out.printf("%s %-8s %4d %7s %7s  %s %s\n",
                        time, new String(DB.TNS).toUpperCase(),
                        DBConnectThread.getNewConnActiveInstance(),
                        Long2TimedString(DBConnectThread.getTCPTime()),
                        Long2TimedString(DBConnectThread.getJDBCTime()),
                        racId,
                        StatementTimes
                        );
                
                /* Print specific instances below main TNS dblink */
                for(Instance i : DB.instances) {
                    ConnectThread iThread = i.cthr;
                    FcfThread fThread = i.fcfthr;
                    txt = fThread.getTransactionTime();
                    exePLSQL = fThread.getExecTime();
                    newRac = fThread.getStatementInstance();
                    oldRac = fThread.getOldStatementInstance();
                    
                    /* handle errors , passed by minus numbers, Long2TimedString()
                     * actually parses it for us
                     */
                    if(txt < 0)  {
                        s_txt = Long2TimedString(txt);
                    } else {
                        s_txt = String.format("%4d", txt);
                    }
                    
                    if(exePLSQL < 0) {
                        s_exePLSQL = Long2TimedString(exePLSQL);
                    } else {
                        s_exePLSQL = String.format("%5d", exePLSQL);
                    }

                    /* we display only the error in case we've got SQLexception */
                    if(txt < 0 || exePLSQL < 0) {
                        StatementTimes = String.format("%21s", s_txt);
                    } else {
                        StatementTimes = String.format("%s %s  %4d %4d", s_txt, s_exePLSQL, cs, rs);
                    }
                    
                    racId = StringRACid(oldRac, newRac);
                    
                    System.out.printf("%s +%-7s %4d %7s %7s  %s %s\n",
                            time, new String(i.TNS).toUpperCase(),
                            iThread.getNewConnActiveInstance(),
                            Long2TimedString(iThread.getTCPTime()),
                            Long2TimedString(iThread.getJDBCTime()),
                            racId,
                            StatementTimes
                            );
                    
                } /* foreach i in DB.instances */
                
                /* monitor for file existence, read & print first line to stderr, delete file */
                if(conf.fileTrigger != null) {
                    try {
                        FileReader fr = new FileReader(conf.fileTrigger);
                        BufferedReader br = new BufferedReader(fr);
                        String line = br.readLine();
                        System.err.println(time + " MARK \"" + line +"\"");
                        fr.close();
                        if (! (new File(conf.fileTrigger)).delete()) {
                            /* deletion failed */
                        }
                        
                    } catch(Exception e) {
                    }
                    
                }
            }
            
            
            Sleep(conf.delay);
            first = false;
        }
        
    }
    
    
    /**
     * @param args the command line arguments
     */
    public static void main(String[] args) {
        Main m = new Main(args);
    }
    
}
